package com.medusa.aps.business.modules.basic.mp.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.medusa.aps.business.config.SqlContext;
import com.medusa.aps.business.model.constant.RedisConstants;
import com.medusa.aps.business.model.exception.BusinessErrorCode;
import com.medusa.aps.business.model.pagination.Page;
import com.medusa.aps.business.modules.basic.model.MonthDayWeek;
import com.medusa.aps.business.modules.basic.model.WorkPeriod;
import com.medusa.aps.business.modules.basic.model.input.ClassSystemParam;
import com.medusa.aps.business.modules.basic.model.input.WorkingCalendarPageParam;
import com.medusa.aps.business.modules.basic.model.input.WorkingDayPeriodParam;
import com.medusa.aps.business.modules.basic.model.output.CalendarModel;
import com.medusa.aps.business.modules.basic.model.output.CalendarWithHeaderModel;
import com.medusa.aps.business.modules.basic.model.output.WorkingCalendarModel;
import com.medusa.aps.business.modules.basic.mp.entity.ClassSystem;
import com.medusa.aps.business.modules.basic.mp.entity.WorkingCalendar;
import com.medusa.aps.business.modules.basic.mp.entity.WorkingPeriod;
import com.medusa.aps.business.modules.basic.mp.mapper.WorkingCalendarMapper;
import com.medusa.aps.business.modules.basic.mp.service.ClassSystemService;
import com.medusa.aps.business.modules.basic.mp.service.WorkingCalendarService;
import com.medusa.aps.business.modules.basic.mp.service.WorkingPeriodService;
import com.medusa.aps.business.modules.modeling.mp.service.EquipmentService;
import com.medusa.aps.business.modules.plan.mp.service.ProcessDetailsService;
import com.medusa.aps.business.util.PercentageScheduleUtils;
import com.medusa.aps.business.common.redis.annotation.Redisson;
import com.medusa.aps.business.global.model.exception.GlobalException;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSession;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.Connection;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;

/**
 * 工作日历服务实现类
 *
 * @author yuxh
 * @since 2023-08-14 17:37:53
 */
@Slf4j
@Service("workingCalendarService")
public class WorkingCalendarServiceImpl extends ServiceImpl<WorkingCalendarMapper, WorkingCalendar> implements WorkingCalendarService {

    @Resource
    private ProcessDetailsService processDetailsService;
    @Resource
    private WorkingPeriodService workingPeriodService;
    @Resource
    private EquipmentService equipmentService;
    @Resource
    private ClassSystemService classSystemService;
    @Resource
    private SqlContext sqlContext;

    /**
     * 渲染表头
     *
     * @param startDate 开始时间
     * @param endDate   结束时间
     * @return 表头
     */
    private SortedSet<MonthDayWeek> renderHeaders(LocalDate startDate, LocalDate endDate) {
        SortedSet<MonthDayWeek> headers = new TreeSet<>(Comparator.comparingInt(MonthDayWeek::getDay));
        if (startDate == null || endDate == null) {
            return headers;
        }
        if (endDate.isBefore(startDate)) {
            return headers;
        }
        startDate.datesUntil(endDate.plusDays(1)).forEach(date -> headers.add(MonthDayWeek.of(date)));
        return headers;
    }


    @Override
    public CalendarWithHeaderModel calendarPage(WorkingCalendarPageParam query) {
        YearMonth month = query.getMonth();
        //开始时间结束时间初始化
        LocalDate startDate = LocalDate.now();
        LocalDate endDate = YearMonth.now().atEndOfMonth();
        if (month != null) {
            startDate = month.atDay(1);
            endDate = month.atEndOfMonth();
        }
        //渲染表头
        SortedSet<MonthDayWeek> headers = renderHeaders(startDate, endDate);
        //根据设备 id 分组 分页查询
        Page<WorkingCalendarModel> devicePage = baseMapper.deviceCalendarPage(query, startDate, endDate);
        List<WorkingCalendarModel> records = devicePage.getRecords();
        //不为空 查询设备日历
        if (CollUtil.isNotEmpty(records)) {
            //取出所有的设备id集合
            Set<Long> deviceIds = records.stream().map(WorkingCalendarModel::getDeviceId).collect(Collectors.toSet());
            List<WorkingCalendar> workingCalendarList = baseMapper.selectDeviceCalendar(deviceIds, startDate, endDate);
            //根据设备id 开始结束日期 查询所有设备日历 并根据设备 id 分组
            Map<Long, List<WorkingCalendar>> deviceCalendarMap = workingCalendarList.stream()
                    .collect(Collectors.groupingBy(WorkingCalendar::getDeviceId));
            records.forEach(
                    record -> {
                        //取出当前设备的工作日历
                        List<WorkingCalendar> workingCalendars = deviceCalendarMap.get(record.getDeviceId());
                        //根据日期分组
                        Map<LocalDate, CalendarModel> dateCalendarMap = CollUtil.emptyIfNull(workingCalendars)
                                .stream()
                                .collect(
                                        Collectors.toMap(
                                                WorkingCalendar::getDate,
                                                calendar -> new CalendarModel()
                                                        .setId(calendar.getId())
                                                        .setIsEdit(calendar.getIsEdit())
                                                        .setPeriodId(calendar.getWorkingTimeId())
                                                        .setPeriodInfo(calendar.getPeriodInfo())
                                        )
                                );
                        record.setCalendars(dateCalendarMap);
                    }
            );

        }
        return new CalendarWithHeaderModel()
                .setHeaders(headers)
                .setCalendarPage(devicePage);
    }

    /**
     * 编辑工作日历
     *
     * @param calendarModel 编辑工作日历参数
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    @Redisson(name = RedisConstants.CLASS_SYSTEM_LOCK_KEY, waitTime = 1)
    public void updateCalendar(CalendarModel calendarModel) {
        PercentageScheduleUtils.validClassSystemGenerateCalendar();
        WorkingCalendar workingCalendar = baseMapper.selectById(calendarModel.getId());
        if (BeanUtil.isEmpty(workingCalendar)) {
            throw new GlobalException(BusinessErrorCode.DATA_IS_EMPTY.getCode(), "工作日历数据不存在");
        }
        //当前日期是否已排程
        Boolean existProcessPlanEquipment = processDetailsService.isExistProcessPlanEquipment(workingCalendar.getDeviceId(), workingCalendar.getDate());
        if (existProcessPlanEquipment) {
            throw new GlobalException(BusinessErrorCode.DATA_IS_USED.getCode(), "当前日期已排程，不支持编辑!");
        }
        WorkingPeriod after;
        if (calendarModel.getPeriodId() == 0) {
            after = new WorkingPeriod();
        } else {
            after = workingPeriodService.getById(calendarModel.getPeriodId());
        }
        workingCalendar.setWorkingTimeId(calendarModel.getPeriodId())
                .setPeriodInfo(
                        calendarModel.getPeriodId() == 0 ? null :
                                new WorkingCalendar.WorkingTimeInfo()
                                        .setName(after.getWorkTimeName())
                                        .setTimes(WorkPeriod.str2Periods(after.getStartAndEndTimeIntervalSet()))
                );
        this.lambdaUpdate()
                .eq(WorkingCalendar::getId, workingCalendar.getId())
                .set(WorkingCalendar::getWorkingTimeId, workingCalendar.getWorkingTimeId())
                .set(WorkingCalendar::getPeriodInfo, JSON.toJSONString(workingCalendar.getPeriodInfo()))
                .update();
    }

    @Override
    @Redisson(name = RedisConstants.CLASS_SYSTEM_LOCK_KEY, waitTime = 30)
    public void generateWorkingCalendar() {
        List<ClassSystem> classSystems = classSystemService.lambdaQuery()
                .select(ClassSystem::getId, ClassSystem::getDeviceIds, ClassSystem::getPeriodIds)
                .list();
        if (CollUtil.isEmpty(classSystems)) {
            return;
        }
        //查询是否已经延长工作日历，防止数据重复
        boolean flag = lambdaQuery()
                .select(WorkingCalendar::getDate)
                .ge(WorkingCalendar::getDate, LocalDate.now())
                .exists();
        if (flag) {
            log.error("工作日历已存在延长数据，本次延长不生效:{}", LocalDateTime.now());
            return;
        }
        Set<Integer> periodIds = classSystems.stream().flatMap(item -> item.getPeriodIds().stream()).collect(Collectors.toSet());
        Map<Integer, WorkingPeriod> workingPeriodMap = workingPeriodService.lambdaQuery()
                .select(WorkingPeriod::getId, WorkingPeriod::getWorkTimeName, WorkingPeriod::getStartAndEndTimeIntervalSet)
                .in(WorkingPeriod::getId, periodIds)
                .list().stream().collect(Collectors.toMap(WorkingPeriod::getId, v -> v));
        List<WorkingCalendar> workingCalendars = classSystems.stream().map(classSystem -> {
            List<Integer> classSystemPeriodIds = classSystem.getPeriodIds();
            SortedSet<WorkingDayPeriodParam> workingDayPeriods = new TreeSet<>();
            for (int i = 0; i < classSystemPeriodIds.size(); i++) {
                workingDayPeriods.add(
                        new WorkingDayPeriodParam()
                                .setWorkingDay(i + 1)
                                .setPeriodId(classSystemPeriodIds.get(i))
                );
            }
            Set<Long> deviceIds = classSystem.getDeviceIds();
            ClassSystemParam classSystemParam = new ClassSystemParam()
                    .setWorkingDayPeriods(workingDayPeriods)
                    .setEquipmentIds(deviceIds);
            return classSystemService.renderCalendar(classSystem.getId(), LocalDate.now(), classSystemParam, workingPeriodMap);
        }).flatMap(Collection::stream).toList();
        SqlSession sqlSession = sqlContext.getSqlSession();
        Connection connection = sqlSession.getConnection();
        try {
            connection.setAutoCommit(false);
            saveWorkingCalendars(workingCalendars);
            connection.commit();
        } catch (Exception e) {
            log.error("事务回滚失败:", e);
            sqlContext.rollback(connection);
            throw new GlobalException("事务回滚失败");
        } finally {
            sqlContext.close(connection, sqlSession);
        }
    }

    @Override
    public void saveWorkingCalendars(List<WorkingCalendar> workingCalendars) throws ExecutionException, InterruptedException {
        List<List<WorkingCalendar>> avg = ListUtil.splitAvg(workingCalendars, 4);
        List<CompletableFuture<Void>> completableFutures = avg.stream().map(item -> CompletableFuture.runAsync(() -> baseMapper.saveBatch(item))).toList();
        CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture[]{})).get();
    }

    /**
     * 根据指定时间与班制id删除工作日历
     * @param classId 班制id
     * @param localDate 删除的时间
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void delCalendarByLimit(Integer classId, LocalDate localDate) {
        //查询数据条数
        Long count = lambdaQuery()
                .select(WorkingCalendar::getClassId)
                .eq(WorkingCalendar::getClassId, classId)
                .ge(WorkingCalendar::getDate, localDate)
                .count();
        if (count == 0){
            return;
        }
        int size = BigDecimal.valueOf(count).divide(BigDecimal.valueOf(50000), 0, RoundingMode.CEILING).intValue();
        for (int i = 0; i < size; i++) {
            lambdaUpdate()
                    .eq(WorkingCalendar::getClassId, classId)
                    .ge(WorkingCalendar::getDate, localDate)
                    .last("LIMIT 50000")
                    .remove();
        }

    }
}

